////////////////////////////////////////////////////////////////////////////////
//
// Licensed under the MIT License
//
// Copyright (c) 2010 Chris Colinsky
// All Rights Reserved
//
// version	0.5
// http://code.google.com/p/sugarcookie/
// http://code.google.com/p/sugarcookie/wiki/License
//
////////////////////////////////////////////////////////////////////////////////

package sugarcookie.utils {
	import mx.logging.ILogger;
	import mx.logging.Log;
	import mx.utils.ObjectUtil;
	
	import sugarcookie.collections.EnhancedArrayCollection;
	import sugarcookie.notifications.Notification;
	import sugarcookie.utils.IObservable;
	import sugarcookie.utils.IObserver;
	import sugarcookie.utils.StringUtil;
	
	/**
	 * A Java-style Observable class used to represent the "subject"
	 * of the Observer design pattern.  Observers must implement the IObserver
	 * interface and register to observe the subject via addObserver().
	 */
	public class Observable implements IObservable {
		// logger
		private var _log:ILogger;
		// flag indicating whether this object has changed.
		private var _changed:Boolean = false;
		// list of observers by type
		private var _observers:EnhancedArrayCollection;
		// notifier object
		private var _notifier:*;
		//log notifications
		private var _useLogging:Boolean = false;
		
		/**
		 * Constructor function
		 **/
		public function Observable(notifier:*=null) {
			_notifier = notifier!=null?notifier:this;
			
			_log = Log.getLogger(StringUtil.getFormattedClassName(_notifier));
			
			_observers = new EnhancedArrayCollection();
			_observers.addItemWithKey("all",[]);
		}
		
		/**
		 * Determies if the notification object is output to the logger for debugging.
		 */
		public function get useLogging():Boolean { return _useLogging; }
		public function set useLogging(value:Boolean):void { _useLogging = value; }
		
		/**
		 * Adds an observer to the list of observers.
		 * @param   o   The observer to be added.
		 **/
		public function addObserver(observer:IObserver,notificationType:String="all"):Boolean {
			// Can't adda null observer.
			if (observer==null){
				return false;
			}
			
			//make sure type exists in registry
			if (!_observers.containsKey(notificationType)) _observers.addItemWithKey(notificationType,[]);
			
			//check to see if object is already listening to all
			var i:Number;
			var arr:Array = _observers.getItemAtKey("all") as Array;
			for (i=0;i<arr.length;i++){
				if (arr[i]==observer){
					// the observer is already observing for all types
					_log.warn("Observer {0} is already registered for 'all' notifications.",StringUtil.getFormattedClassName(observer));
					return false;
				}
			}
			
			// Don't add an observer more than once
			if (notificationType!="all") {
				arr = _observers.getItemAtKey(notificationType) as Array;
				for (i=0;i<arr.length;i++){
					if (arr[i]==observer){
						// the observer is already observing for this type
						_log.warn("Observer {0} is already registered for '{1}' notifications.",StringUtil.getFormattedClassName(observer),notificationType);
						return false;
					}
				}
			}
			
			// Put the observer in the list
			arr.push(observer);
			return true;
		}
		
		/**
		 * Removes an observer from the list of observers for a specific type or all.
		 * 
		 * @param     o     The observer to remove
		 **/
		public function removeObserver(observer:IObserver,notificationType:String="all"):Boolean {
			// Find and remove the observer.
			var arr:Array = _observers.source;
			for (var s:String in arr){
				for (var i:int=0;i<arr[s].length;i++) {
					if (arr[s][i]==observer && s==notificationType){
						arr[s].splice(i,1);
						_log.debug("Observer {0} has been removed from {1} notifications",StringUtil.getFormattedClassName(observer),notificationType);
						return true;
					}
				}
			}
			return false;
		}
		
		/**
		 * tell all observers that the subject has changed.
		 *
		 * @param     infoObj     An object containing arbitrary data to pass to observers.
		 **/
		public function notifyObservers(type:String,data:Object=null):void {
			// if the subject hasn't changed, don't bother notifying observers.
			if (!_changed){
				return;
			}
			
			//check to make sure there are observers
			var key:String = type;
			if (!_observers.containsKey(key)) {
				if (!_observers.containsKey("all")) {
					_log.warn("There are no observers for notification type: {0}",type);
					return;
				} else {
					key = "all";
				}
			}
			
			// Make a copy of the observers array. done to ensure
			// the list doesn't change while we're processing it.
			var i:int;
			var ref:Array = _observers.getItemAtKey(key) as Array;
			var observersSnapshot:Array = ref.concat();
			
			if (key!="all") ref = _observers.getItemAtKey("all") as Array;
			var all:Array = ref.concat();
			for (i=0;i<all.length;i++) {
				var isMember:Boolean = false;
				for (var n:int=0;n<observersSnapshot.length;n++) {
					if (all[i]===observersSnapshot[n]) {
						isMember = true;
						break;
					}
				}
				if (!isMember) observersSnapshot.push(all[i]);
			}
			
			// This change has been processed, so unset the changed flag.
			clearChanged();
			
			//create notification object to be sent out
			var notification:Notification = new Notification(type,_notifier,data);
			
			//if logging is on, do output
			if (_useLogging) _log.info(notification.type + " notification from "+ notification.notifier +": "+ObjectUtil.toString(notification.data));
			
			// invoke the update() on all observers.
			for (i=observersSnapshot.length-1;i>=0;i--) observersSnapshot[i].update(notification);
		}
		
		/**
		 * Removes all observers from the onserver list.
		 **/
		public function clearObservers():void {
			_observers.removeAll();
		}
		
		/**
		 * Indicates that the subject has changed.
		 **/
		public function setChanged():void {
			_changed = true;
		}
		
		/**
		 * Indicates that the subject has either not changed or
		 * has notified its observers of the most recent change.
		 **/
		public function clearChanged():void {
			_changed = false;
		}
		
		/**
		 * Checks if the subject has changed.
		 *
		 * @param     true if the subject has changed, as determined by setChange().
		 **/
		public function hasChanged():Boolean {
			return _changed;
		}
		
		/**
		 * Returns the number of observers in the observer list.
		 *
		 * @return     An interger: the number of observers for this subject.
		 **/
		public function countObservers():Number {
			return _observers.length;
		}
	}
}